﻿using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Context;

namespace Infrastructure.Data.NH.Core.Util
{
    /// <summary>
    /// A static (singleton) class to manage NHibernate.
    /// Manage NHibernate sessions using <see cref="Session"/>
    /// </summary>
    public sealed class SessionManager
    {
        [ThreadStatic]
        private static ISession _session;
        private readonly Configuration configuration;
        private readonly ISessionFactory sessionFactory;

        #region Constructor

        #region Singleton
        public static SessionManager Instance
        {
            get
            {
                return Singleton.Instance;
            }
        }

        //#if !INSTANCE
        private class Singleton
        {
            static Singleton()
            {

            }
            internal static readonly SessionManager Instance = new SessionManager();
        }
        //#endif
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="SessionManager"/> class.
        /// </summary>
        private SessionManager()
        {
            ConfigurationFilePath = Path.Combine(
                Path.GetDirectoryName(
                new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath),
                             "Configuration.save");
            configuration = RestoreConfiguration();
            if (configuration == null)
            {
                configuration = new Configuration();
                BuildConfiguration();
            }
            //get the session factory
            sessionFactory = Configuration.BuildSessionFactory();
        }

        private void BuildConfiguration()
        {
            Configuration.Configure(); //configure from the app.config

            //Configuration.AddAssembly ( GetType ( ).Assembly );//default- mapping is in this assembly
            //other examples:
            //Configuration.AddFile("file.hbm.xml"); //add files
            //Configuration.AddAssembly(assembly); //add assembly
            //Configuration.AddAssembly(assemblyName); //add assembly by name
            //foreach (string assemblyName in assemblyNames) //add enumerable of assemblies
            //    Configuration.AddAssembly(assemblyName);
            //#if !DEBUG
            SaveConfiguration(Configuration);
            //#endif
        }
        #endregion

        #region Configuration Serialization
        //specify a full path if required
        private static string ConfigurationFilePath;
        private static void SaveConfiguration(Configuration configuration)
        {
            IFormatter serializer = new BinaryFormatter();

            using (Stream stream = File.OpenWrite(ConfigurationFilePath))
            {
                try
                {
                    serializer.Serialize(stream, configuration);
                }
                catch (UnauthorizedAccessException)
                {
                    Console.WriteLine("Error saving the Configuration: No write access to Configuration file");
                }
            }
        }

        /// <summary>
        /// Optimization- you could deploy the serialization file.
        /// </summary>
        private static Configuration RestoreConfiguration()
        {
#if DEBUG
            return null;
#endif
            // todo check if the saved config is the same to the config file
            try
            {
                if (!File.Exists(ConfigurationFilePath)) return null;
                IFormatter serializer = new BinaryFormatter();

                using (Stream stream = File.OpenRead(ConfigurationFilePath))
                {
                    return serializer.Deserialize(stream) as Configuration;
                }
            }
            catch (Exception)
            {
                return null;
            }
        }
        #endregion

        #region NHibernate Setup
        /// <summary>
        /// Gets the <see cref="NHibernate.Cfg.Configuration"/>.
        /// </summary>
        internal Configuration Configuration { get { return configuration; } }

        /// <summary>
        /// Gets the <see cref="NHibernate.ISessionFactory"/>
        /// </summary>
        internal ISessionFactory SessionFactory { get { return sessionFactory; } }

        /// <summary>
        /// Closes the session factory.
        /// </summary>
        public void Close()
        {
            SessionFactory.Close();
        }

        #endregion

        #region NHibernate SessionContext (1.2+)

        /// <summary>
        /// Opens the conversation (if already existing, reuses it). Call this from Application_BeginPreRequestHandlerExecute
        /// </summary>
        /// <returns>A <see cref="NHibernate.ISession"/></returns>
        public ISession OpenConversation()
        {
            //you must set <property name="current_session_context_class">web</property> (or thread_static etc)
            //for session per conversation (otherwise remove)
            _session.FlushMode = FlushMode.Never; //Only save on session.Flush() - because we need to commit on unbind in PauseConversation
            _session.BeginTransaction(); //start a transaction
            CurrentSessionContext.Bind(_session); //bind it
            return _session;
        }

        /// <summary>
        /// Ends the conversation. If an exception occurs, rethrows it ensuring session is closed. Call this (or <see cref="PauseConversation"/> if session per conversation) from Application_PostRequestHandlerExecute
        /// </summary>
        public void EndConversation(bool rollback)
        {
            ISession session = CurrentSessionContext.Unbind(SessionFactory);
            if (session == null) return;
            try
            {
                if (rollback)
                    session.Transaction.Rollback();
                else
                {
                    session.Flush();
                    session.Transaction.Commit();
                }
            }
            catch (Exception)
            {
                if (!rollback)
                    session.Transaction.Rollback();
                throw;
            }
            finally
            {
                session.Close();
            }
        }

        /// <summary>
        /// Gets the current conversation.
        /// </summary>
        /// <value>The current conversation.</value>
        public ITransaction CurrentConversation { get { return _session.Transaction; } }

        /// <summary>
        /// Pauses the conversation. Call this (or <see cref="EndConversation"/>) from Application_EndRequest
        /// </summary>
        public void PauseConversation()
        {
            ISession session = CurrentSessionContext.Unbind(SessionFactory);
            if (session == null) return;
            try
            {
                session.Transaction.Commit(); //with flushMode=Never, this closes connections but doesn't flush
            }
            catch (Exception)
            {
                session.Transaction.Rollback();
                throw;
            }
            //we don't close the session, and it's still in Asp SessionState
        }
        #endregion

        #region NHibernate Sessions

        /// <summary>
        /// Explicitly open a session. If you have an open session, close it first.
        /// </summary>
        /// <returns>The <see cref="NHibernate.ISession"/></returns>
        public ISession OpenSession()
        {
            ISession session = SessionFactory.OpenSession();
            SessionManager._session = session;
            return session;
        }

        /// <summary>
        /// Gets the current <see cref="NHibernate.ISession"/>. Although this is a singleton, this is specific to the thread/ asp session. If you want to handle multiple sessions, use <see cref="OpenSession"/> directly. If a session it not open, a new open session is created and returned.
        /// </summary>
        /// <value>The <see cref="NHibernate.ISession"/></value>
        public ISession Session
        {
            get
            {
                //use threadStatic or asp session.
                ISession session = SessionManager._session;
                //if using CurrentSessionContext, SessionFactory.GetCurrentSession() can be used

                //if it's an open session, that's all
                if (session != null && session.IsOpen)
                    return session;

                //if not open, open a new session
                return OpenSession();
            }
        }
        #endregion

    }
}
